package com.quanxiaoha.xiaohashu.note.biz.service.impl;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.RandomUtil;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.google.common.base.Preconditions;
import com.quanxiaoha.framework.biz.context.holder.LoginUserContextHolder;
import com.quanxiaoha.framework.common.exception.BizException;
import com.quanxiaoha.framework.common.response.Response;
import com.quanxiaoha.framework.common.util.JsonUtils;
import com.quanxiaoha.framework.redis.constant.RedisKeyConstants;
import com.quanxiaoha.xiaohashu.note.biz.constant.MQConstants;
import com.quanxiaoha.xiaohashu.note.biz.domain.dataobject.NoteDO;
import com.quanxiaoha.xiaohashu.note.biz.domain.dataobject.TopicDO;
import com.quanxiaoha.xiaohashu.note.biz.enums.NoteStatusEnum;
import com.quanxiaoha.xiaohashu.note.biz.enums.NoteTypeEnum;
import com.quanxiaoha.xiaohashu.note.biz.enums.NoteVisibleEnum;
import com.quanxiaoha.xiaohashu.note.biz.enums.ResponseCodeEnum;
import com.quanxiaoha.xiaohashu.note.biz.mapper.NoteDOMapper;
import com.quanxiaoha.xiaohashu.note.biz.model.vo.*;
import com.quanxiaoha.xiaohashu.note.biz.rpc.DistributedIdGeneratorRpcService;
import com.quanxiaoha.xiaohashu.note.biz.rpc.KeyValueRpcService;
import com.quanxiaoha.xiaohashu.note.biz.rpc.UserRpcService;
import com.quanxiaoha.xiaohashu.note.biz.service.NoteDOService;
import com.quanxiaoha.xiaohashu.note.biz.service.TopicDOService;
import com.quanxiaoha.xiaohashu.user.dto.resp.FindUserByIdRspDTO;
import jakarta.annotation.Resource;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.rocketmq.spring.core.RocketMQTemplate;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;

/**
 * @author jingyanliu
 * @description 针对表【t_note(笔记表)】的数据库操作Service实现
 * @createDate 2025-07-30 15:42:49
 */
@Service
@Slf4j
public class NoteDOServiceImpl extends ServiceImpl<NoteDOMapper, NoteDO>
		implements NoteDOService {
	
	private static final Cache<Long, String> LOCAL_CACHE = Caffeine.newBuilder()
			.initialCapacity(10000) // 设置初始容量为 10000 个条目
			.maximumSize(10000) // 设置缓存的最大容量为 10000 个条目
			.expireAfterWrite(1, TimeUnit.HOURS) // 设置缓存条目在写入后 1 小时过期
			.build();
	@Resource
	private TopicDOService topicDOService;
	@Resource
	private DistributedIdGeneratorRpcService distributedIdGeneratorRpcService;
	@Resource
	private KeyValueRpcService keyValueRpcService;
	@Resource
	private UserRpcService userRpcService;
	@Resource
	private ThreadPoolTaskExecutor taskExecutor;
	@Resource
	private RedisTemplate<String, String> redisTemplate;
	@Resource
	private RocketMQTemplate rocketMQTemplate;
	
	@Override
	public Response<?> publishNote(PublishNoteReqVO publishNoteReqVO) {
		// 笔记类型
		Integer type = publishNoteReqVO.getType();
		// 获取对应类型的枚举
		NoteTypeEnum noteTypeEnum = NoteTypeEnum.valueOf(type);
		// 若非图文、视频，抛出业务业务异常
		if (Objects.isNull(noteTypeEnum)) {
			throw new BizException(ResponseCodeEnum.NOTE_TYPE_ERROR);
		}
		String imgUris = null;
		// 笔记内容是否为空，默认值为 true，即空
		boolean isContentEmpty = true;
		String videoUri = null;
		switch (noteTypeEnum) {
			case IMAGE_TEXT -> { // 图文笔记
				List<String> imgUriList = publishNoteReqVO.getImgUris();
				// 校验图片是否为空
				Preconditions.checkArgument(CollUtil.isNotEmpty(imgUriList), "笔记图片不能为空");
				// 校验图片数量
				Preconditions.checkArgument(imgUriList.size() <= 8, "笔记图片不能多于 8 张");
				// 将图片链接拼接，以逗号分隔
				imgUris = StringUtils.join(imgUriList, ",");
			}
			case VIDEO -> { // 视频笔记
				videoUri = publishNoteReqVO.getVideoUri();
				// 校验视频链接是否为空
				Preconditions.checkArgument(StringUtils.isNotBlank(videoUri), "笔记视频不能为空");
			}
		}
		// RPC: 调用分布式 ID 生成服务，生成笔记 ID
		String snowflakeIdId = distributedIdGeneratorRpcService.getSnowflakeId();
		// 笔记内容 UUID
		String contentUuid = null;
		// 笔记内容
		String content = publishNoteReqVO.getContent();
		// 若用户填写了笔记内容
		if (StringUtils.isNotBlank(content)) {
			// 内容是否为空，置为 false，即不为空
			isContentEmpty = false;
			// 生成笔记内容 UUID
			contentUuid = UUID.randomUUID().toString();
			// RPC: 调用 KV 键值服务，存储短文本
			boolean isSavedSuccess = keyValueRpcService.saveNoteContent(contentUuid, content);
			// 若存储失败，抛出业务异常，提示用户发布笔记失败
			if (!isSavedSuccess) {
				throw new BizException(ResponseCodeEnum.NOTE_PUBLISH_FAIL);
			}
		}
		// 话题
		Long topicId = publishNoteReqVO.getTopicId();
		String topicName = null;
		if (Objects.nonNull(topicId)) {
			// 获取话题名称
			topicName = topicDOService.lambdaQuery()
					.select(TopicDO::getName)
					.eq(TopicDO::getId, topicId)
					.one().getName();
		}
		// 发布者用户 ID
		Long creatorId = LoginUserContextHolder.getUserId();
		// 构建笔记 DO 对象
		NoteDO noteDO = NoteDO.builder()
				.id(Long.valueOf(snowflakeIdId))
				.isContentEmpty(isContentEmpty)
				.creatorId(creatorId)
				.imgUris(imgUris)
				.title(publishNoteReqVO.getTitle())
				.topicId(publishNoteReqVO.getTopicId())
				.topicName(topicName)
				.type(type)
				.visible(NoteVisibleEnum.PUBLIC.getCode())
				.status(NoteStatusEnum.NORMAL.getCode())
				.isTop(Boolean.FALSE)
				.videoUri(videoUri)
				.contentUuid(contentUuid)
				.build();
		try {
			// 笔记入库存储
			this.save(noteDO);
		} catch (Exception e) {
			log.error("==> 笔记存储失败", e);
			// RPC: 笔记保存失败，则删除笔记内容
			if (StringUtils.isNotBlank(contentUuid)) {
				keyValueRpcService.deleteNoteContent(contentUuid);
			}
		}
		return Response.success();
	}
	
	@Override
	public void deleteNoteLocalCache(Long noteId) {
		LOCAL_CACHE.invalidate(noteId);
	}
	
	@Override
	@Transactional(rollbackFor = Exception.class)
	public Response<?> deleteNote(DeleteNoteReqVO deleteNoteReqVO) {
		// 笔记 ID
		Long noteId = deleteNoteReqVO.getId();
		// 逻辑删除
		boolean isUpdateSuccess = this.lambdaUpdate()
				.set(NoteDO::getStatus, NoteStatusEnum.DELETED.getCode())
				.eq(NoteDO::getId, noteId)
				.eq(NoteDO::getStatus, NoteStatusEnum.NORMAL.getCode())
				.update();
		if (!isUpdateSuccess) {
			throw new BizException(ResponseCodeEnum.NOTE_NOT_FOUND);
		}
		// 删除缓存
		String noteDetailRedisKey = RedisKeyConstants.buildNoteDetailKey(noteId);
		redisTemplate.delete(noteDetailRedisKey);
		log.info("====> Redis：删除笔记 {} 缓存成功...", noteId);
		// 同步发送广播模式 MQ，将所有实例中的本地缓存都删除掉
		rocketMQTemplate.syncSend(MQConstants.TOPIC_DELETE_NOTE_LOCAL_CACHE, noteId);
		log.info("====> MQ：删除笔记 {} 本地缓存成功...", noteId);
		return Response.success();
	}
	
	@Override
	public Response<?> visibleOnlyMe(UpdateNoteVisibleOnlyMeReqVO updateNoteVisibleOnlyMeReqVO) {
		// 笔记 ID
		Long noteId = updateNoteVisibleOnlyMeReqVO.getId();
		// 执行更新 SQL
		boolean isUpdateSuccess = this.lambdaUpdate()
				.set(NoteDO::getVisible, NoteVisibleEnum.PRIVATE.getCode())
				.eq(NoteDO::getId, noteId)
				.eq(NoteDO::getStatus, NoteStatusEnum.NORMAL.getCode())
				.update();
		if (!isUpdateSuccess) {
			throw new BizException(ResponseCodeEnum.NOTE_CANT_VISIBLE_ONLY_ME);
		}
		// 删除 Redis 缓存
		String noteDetailRedisKey = RedisKeyConstants.buildNoteDetailKey(noteId);
		redisTemplate.delete(noteDetailRedisKey);
		log.info("====> Redis：删除笔记 {} 缓存成功...", noteId);
		
		// 同步发送广播模式 MQ，将所有实例中的本地缓存都删除掉
		rocketMQTemplate.syncSend(MQConstants.TOPIC_DELETE_NOTE_LOCAL_CACHE, noteId);
		log.info("====> MQ：删除笔记 {} 本地缓存成功...", noteId);
		return Response.success();
	}
	
	@Override
	@SneakyThrows
	public Response<FindNoteDetailRspVO> findNoteDetail(FindNoteDetailReqVO findNoteDetailReqVO) {
		// 查询的笔记 ID
		Long noteId = findNoteDetailReqVO.getId();
		// 当前登录用户
		Long userId = LoginUserContextHolder.getUserId();
		// 先从本地缓存中查询
		String findNoteDetailRspVOStrLocalCache = LOCAL_CACHE.getIfPresent(noteId);
		if (StringUtils.isNotBlank(findNoteDetailRspVOStrLocalCache)) {
			log.info("==> 命中了本地缓存；{}", findNoteDetailRspVOStrLocalCache);
			FindNoteDetailRspVO findNoteDetailRspVO = JsonUtils.parseObject(findNoteDetailRspVOStrLocalCache, FindNoteDetailRspVO.class);
			checkNoteVisibleFromVO(userId, findNoteDetailRspVO);
			return Response.success(findNoteDetailRspVO);
		}
		
		// 从 Redis 缓存中获取
		String noteDetailRedisKey = RedisKeyConstants.buildNoteDetailKey(noteId);
		String noteDetailJson = redisTemplate.opsForValue().get(noteDetailRedisKey);
		// 若缓存中有该笔记的数据，则直接返回
		if (StringUtils.isNotBlank(noteDetailJson)) {
			log.info("==> 命中了redis缓存；{}", noteDetailJson);
			FindNoteDetailRspVO findNoteDetailRspVO = JsonUtils.parseObject(noteDetailJson, FindNoteDetailRspVO.class);
			// 异步线程中将用户信息存入本地缓存
			taskExecutor.submit(() -> {
				// 写入本地缓存
				LOCAL_CACHE.put(noteId,
						Objects.isNull(findNoteDetailRspVO) ? "null" : JsonUtils.toJsonString(findNoteDetailRspVO));
			});
			// 可见性校验
			checkNoteVisibleFromVO(userId, findNoteDetailRspVO);
			return Response.success(findNoteDetailRspVO);
		}
		// 若 Redis 缓存中获取不到，则走数据库查询
		NoteDO noteDO = this.lambdaQuery()
				.eq(NoteDO::getId, noteId)
				.eq(NoteDO::getStatus, NoteStatusEnum.NORMAL.getCode())
				.one();
		
		// 若该笔记不存在，则抛出业务异常
		if (Objects.isNull(noteDO)) {
			taskExecutor.execute(() -> {
				// 防止缓存穿透，将空数据存入 Redis 缓存 (过期时间不宜设置过长)
				// 保底1分钟 + 随机秒数
				long expireSeconds = 60 + RandomUtil.randomInt(60);
				redisTemplate.opsForValue().set(noteDetailRedisKey, "null", expireSeconds, TimeUnit.SECONDS);
			});
			throw new BizException(ResponseCodeEnum.NOTE_NOT_FOUND);
		}
		
		// RPC: 调用用户服务，获取发布者用户信息
		Long creatorId = noteDO.getCreatorId();
		CompletableFuture<FindUserByIdRspDTO> userResultFuture = CompletableFuture
				.supplyAsync(() -> userRpcService.findById(creatorId), taskExecutor);
		
		// RPC: 调用 KV 键值服务，获取笔记内容
		CompletableFuture<String> contentResultFuture = CompletableFuture.completedFuture(null);
		if (Objects.equals(noteDO.getIsContentEmpty(), Boolean.FALSE)) {
			contentResultFuture = CompletableFuture
					.supplyAsync(() -> keyValueRpcService.findNoteContent(noteDO.getContentUuid()), taskExecutor);
		}
		CompletableFuture<String> finalContentResultFuture = contentResultFuture;
		CompletableFuture<FindNoteDetailRspVO> resultFuture = CompletableFuture
				// 方法用于等待一组 CompletableFuture 全部完成。它返回一个新的 CompletableFuture
				.allOf(userResultFuture, contentResultFuture)
				.thenApply(s -> {
					// 获取 Future 返回的结果
					FindUserByIdRspDTO findUserByIdRspDTO = userResultFuture.join();
					String content = finalContentResultFuture.join();
					
					// 笔记类型
					Integer noteType = noteDO.getType();
					// 图文笔记图片链接(字符串)
					String imgUrisStr = noteDO.getImgUris();
					// 图文笔记图片链接(集合)
					List<String> imgUris = null;
					// 如果查询的是图文笔记，需要将图片链接的逗号分隔开，转换成集合
					if (Objects.equals(noteType, NoteTypeEnum.IMAGE_TEXT.getCode())
							&& StringUtils.isNotBlank(imgUrisStr)) {
						imgUris = List.of(imgUrisStr.split(","));
					}
					return FindNoteDetailRspVO.builder()
							.id(noteDO.getId())
							.type(noteDO.getType())
							.title(noteDO.getTitle())
							.content(content)
							.imgUris(imgUris)
							.topicId(noteDO.getTopicId())
							.topicName(noteDO.getTopicName())
							.creatorId(noteDO.getCreatorId())
							.creatorName(findUserByIdRspDTO.getNickName())
							.avatar(findUserByIdRspDTO.getAvatar())
							.videoUri(noteDO.getVideoUri())
							.visible(noteDO.getVisible())
							.build();
				});
		
		// 获取拼装后的 FindNoteDetailRspVO
		FindNoteDetailRspVO findNoteDetailRspVO = resultFuture.get();
		String noteDetailJson1 = JsonUtils.toJsonString(findNoteDetailRspVO);
		// 异步线程中将笔记详情存入 Redis
		taskExecutor.submit(() -> {
			// 过期时间（保底1天 + 随机秒数，将缓存过期时间打散，防止同一时间大量缓存失效，导致数据库压力太大）
			long expireSeconds = 60 * 60 * 24 + RandomUtil.randomInt(60 * 60 * 24);
			redisTemplate.opsForValue().set(noteDetailRedisKey, noteDetailJson1, expireSeconds, TimeUnit.SECONDS);
			log.info("==> 笔记详情数据已存入 Redis 缓存；{}", noteDetailJson1);
		});
		
		// 异步线程中将用户信息存入本地缓存
		taskExecutor.submit(() -> {
			// 写入本地缓存
			LOCAL_CACHE.put(noteId, noteDetailJson1);
			log.info("==> 笔记详情数据已存入本地缓存；{}", noteDetailJson1);
		});
		return Response.success(findNoteDetailRspVO);
	}
	
	@Override
	@Transactional(rollbackFor = Exception.class)
	public Response<?> updateNote(UpdateNoteReqVO updateNoteReqVO) {
		// 笔记 ID
		Long noteId = updateNoteReqVO.getId();
		// 笔记类型
		Integer type = updateNoteReqVO.getType();
		// 获取对应类型的枚举
		NoteTypeEnum noteTypeEnum = NoteTypeEnum.valueOf(type);
		// 若非图文、视频，抛出业务业务异常
		if (Objects.isNull(noteTypeEnum)) {
			throw new BizException(ResponseCodeEnum.NOTE_TYPE_ERROR);
		}
		String imgUris = null;
		String videoUri = null;
		switch (noteTypeEnum) {
			case IMAGE_TEXT -> { // 图文笔记
				List<String> imgUriList = updateNoteReqVO.getImgUris();
				// 校验图片是否为空
				Preconditions.checkArgument(CollUtil.isNotEmpty(imgUriList), "笔记图片不能为空");
				// 校验图片数量
				Preconditions.checkArgument(imgUriList.size() <= 8, "笔记图片不能多于 8 张");
				// 将图片链接拼接，以逗号分隔
				imgUris = StringUtils.join(imgUriList, ",");
			}
			case VIDEO -> { // 视频笔记
				videoUri = updateNoteReqVO.getVideoUri();
				// 校验视频链接是否为空
				Preconditions.checkArgument(StringUtils.isNotBlank(videoUri), "笔记视频不能为空");
			}
		}
		// 话题
		Long topicId = updateNoteReqVO.getTopicId();
		String topicName = null;
		if (Objects.nonNull(topicId)) {
			topicName = topicDOService.lambdaQuery()
					.select(TopicDO::getName)
					.eq(TopicDO::getId, topicId)
					.one().getName();
			// 判断一下提交的话题, 是否是真实存在的
			if (StringUtils.isBlank(topicName)) throw new BizException(ResponseCodeEnum.TOPIC_NOT_EXIST);
		}
		// 更新笔记元数据表 t_note
		String content = updateNoteReqVO.getContent();
		this.lambdaUpdate().set(NoteDO::getIsContentEmpty, StringUtils.isBlank(content))
				.set(imgUris != null, NoteDO::getImgUris, imgUris)
				.set(NoteDO::getTitle, updateNoteReqVO.getTitle())
				.set(NoteDO::getTopicId, topicId)
				.set(NoteDO::getTopicName, topicName)
				.set(NoteDO::getType, type)
				.set(NoteDO::getVideoUri, videoUri)
				.eq(NoteDO::getId, noteId)
				.eq(NoteDO::getStatus, NoteStatusEnum.NORMAL)
				.update();
		
		// 删除 Redis 缓存
		String noteDetailRedisKey = RedisKeyConstants.buildNoteDetailKey(noteId);
		log.info("删除 Redis 缓存: {}", noteDetailRedisKey);
		redisTemplate.delete(noteDetailRedisKey);
		
		// 同步发送广播模式 MQ，将所有实例中的本地缓存都删除掉
		rocketMQTemplate.syncSend(MQConstants.TOPIC_DELETE_NOTE_LOCAL_CACHE, noteId);
		log.info("====> MQ：删除笔记本地缓存发送成功...");
		
		// 笔记内容更新
		// 查询此篇笔记内容对应的 UUID
		String contentUuid = lambdaQuery().select(NoteDO::getContentUuid)
				.eq(NoteDO::getId, noteId)
				.eq(NoteDO::getStatus, 1)
				.one().getContentUuid();
		
		// 笔记内容是否更新成功
		boolean isUpdateContentSuccess;
		if (StringUtils.isBlank(content)) {
			// 若笔记内容为空，则删除 K-V 存储
			isUpdateContentSuccess = keyValueRpcService.deleteNoteContent(contentUuid);
			log.info("====> 删除 K-V 存储成功；{}", contentUuid);
		} else {
			// 若将无内容的笔记，更新为了有内容的笔记，需要重新生成 UUID
			contentUuid = StringUtils.isBlank(contentUuid) ? UUID.randomUUID().toString() : contentUuid;
			// 调用 K-V 更新短文本
			isUpdateContentSuccess = keyValueRpcService.saveNoteContent(contentUuid, content);
			log.info("====> 更新 K-V 存储成功；{}", contentUuid);
		}
		// 如果更新失败，抛出业务异常，回滚事务
		if (!isUpdateContentSuccess) {
			throw new BizException(ResponseCodeEnum.NOTE_UPDATE_FAIL);
		}
		return Response.success();
	}
	
	private void checkNoteVisible(Integer visible, Long currUserId, Long creatorId) {
		if (Objects.equals(visible, NoteVisibleEnum.PRIVATE.getCode())
				&& !Objects.equals(currUserId, creatorId)) { // 仅自己可见, 并且访问用户为笔记创建者才能访问，非本人则抛出异常
			throw new BizException(ResponseCodeEnum.NOTE_PRIVATE);
		}
	}
	
	private void checkNoteVisibleFromVO(Long userId, FindNoteDetailRspVO findNoteDetailRspVO) {
		if (Objects.nonNull(findNoteDetailRspVO)) {
			Integer visible = findNoteDetailRspVO.getVisible();
			checkNoteVisible(visible, userId, findNoteDetailRspVO.getCreatorId());
		}
	}
}




